Skip to content

fix(plugin-grid): a list row whose click opens nothing draws no record link and no Open item - #33544

Queued
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-9khlxt
Queued

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-9khlxt

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

domain:ui#1 · os-project-manager · session_01FEkQ5pN9UhYM1KGHApVGCH

Fixes #33317

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:列表里点了不会打开任何东西的行(视图写了 navigation.mode: 'none',或只开放 list 的对象),表格第一列不再画成链接(没有链接颜色、下划线、锚点或链接角色),右键菜单也不再出现「打开」和「在新标签页打开」;行操作(如 Revoke)照旧。对象列表、对象数据页、界面列表页三处都生效。
  • 为什么改:这些行点了没有任何反应,但看起来可以点,用户会以为页面坏了。
  • 风险与回滚:表格和列表视图各多一个可选的宿主属性 rowOpensRecord,不传时行为与之前完全一样,其他宿主不受影响;能打开记录的行(抽屉、页面)不变。回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-grid/src/objectGridLinkCell.tsx 的 opens === false 分支;ui/packages/plugin-grid/src/ObjectGrid.tsx 的 renderRowContextMenu 里 rowOpensRecord 两处;ui/packages/app-shell/src/views/ObjectView.tsx 的 rowOpensRecord: navOverlay.mode !== 'none'。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/ObjectView.rowOpensNothing-33317.test.tsx packages/app-shell/src/views/InterfaceListPage.rowOpensNothing-33317.test.tsx,预期 9 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

Measured on origin/main f068cde8 (#33441 included), through the real ObjectView → ListView → ObjectGrid. The console hosts always hand ListView an onRowClick, and ListView always hands the grid onRowClick: navigation.handleClick, so the grid could not tell a row that opens nothing from one that opens a drawer. Two lines decided the face:

  • the record-link cell: objectGridGenerateColumns.tsx wraps the first column (and any link: true column) in LinkCell unconditionally, and RecordLink in objectGridLinkCell.tsx draws a href when the host publishes a record URL, else span role="link". Measured: a view authored navigation: { mode: 'none' } on a served object draws the anchor (the host's recordHref answers); a list-only object draws the span role="link" with data-testid="primary-field-link" (its recordHref answers null).
  • the right-click Open: renderRowContextMenu in ObjectGrid.tsx reads const opens = !!onRowClick || (navigation.mode !== 'none' && ...), so any host handler means Open. Measured: authored none offers row-context-open and row-context-open-new-tab; list-only offers row-context-open.

The fix, the narrowest signal the order allowed: the grid cannot derive it from props it already receives (the console's resolved mode never reaches it; the grid branch of ListView forwards no navigation), so one optional prop carries it, absent = today's behaviour.

  • @object-ui/plugin-grid: ObjectGridComponentProps.rowOpensRecord?: boolean. false ⇒ LinkCell (new internal opens prop) draws the value in a plain span with the link's clamp and width only (no anchor, no role, no handler of its own, so its click falls through to the row), and renderRowContextMenu offers neither Open nor Open in new tab. Row actions, the actions column and fix(plugin-grid): fold a row's inline primary action into its menu while the table overflows with it #33441's inline fold are untouched.
  • @object-ui/plugin-list: ListViewProps.rowOpensRecord?: boolean, forwarded to the grid only, on the grid's host channel beside recordLinkAdornment (only when defined).
  • @object-ui/app-shell (ships in @object-ui/console): ObjectView passes navOverlay.mode !== 'none' (the resolved mode: none for authored none and for a list-only object via servesRecordRead) through useObjectViewListRenderer; ObjectDataPage passes recordServed; InterfaceListPage passes recordAction !== 'none'. onRowClick is passed exactly as before.
  • Docs: rowOpensRecord sections in ui/packages/plugin-grid/README.md, content/docs/frontend/plugins/plugin-grid.mdx and ui/packages/plugin-list/README.md.
  • Changeset .changeset/33317-row-opens-nothing-plain.md: @object-ui/plugin-grid, @object-ui/plugin-list, @object-ui/app-shell, @object-ui/console patch, both declaration lines.

Not changed: packages/spec, any server package, onRowClick routing, row actions.

Tests

ui/packages/app-shell/src/views/ObjectView.rowOpensNothing-33317.test.tsx, real ObjectView + ListView grid + MePermissionsProvider, one differential per case (same object and rows; only the view's navigation or the served operations differ). The right-click menu is opened by a contextmenu event on a plain cell.

  • authored navigation.mode: none: the name cell holds no a[href] and no [role="link"], no primary-field-link; the menu has no Open items and keeps Revoke.
  • authored none: a ⌘-click on a plain cell still reaches the host (one window.open), and a ⌘-click on the now-plain name cell reaches the row too (a second one).
  • list-only object: same as the first case.
  • CONTROL, default drawer: the name cell is the record link, the menu offers Open and Open in new tab and Revoke.
  • CONTROL, authored page: the name cell is the link, the menu offers Open.
  • data page, list-only: the name cell is plain; CONTROL served: it is the link.

ui/packages/app-shell/src/views/InterfaceListPage.rowOpensNothing-33317.test.tsx, real InterfaceListPage + ListView grid: recordAction: 'none' → plain name cell; CONTROL default drawer → link.

Red-first (the measurement): the eight production files restored to base f068cde8 (0 differ from base after restore), the new pins kept.

base sources: 5 failed | 4 passed (9); restored: 0 bytes differ from HEAD
× `recordAction: none`: the name cell is plain text
× authored `navigation.mode: none`: the name cell is plain text, and the right-click menu has no Open but keeps Revoke
× authored `none` still hands the row click to the host: a ⌘-click on the row opens the record in a new tab, as before
× a list-only object: the name cell is plain text, and the right-click menu has no Open but keeps Revoke
× a list-only object: the name cell is plain text
AssertionError: expected true to be false (x4: the name cell wears the link face)
AssertionError: expected "vi.fn()" to be called 2 times, but got 1 times
Test Files  2 failed (2) · Tests  5 failed | 4 passed (9) · VERDICT command-exit 1

The ⌘-click case's first half (a plain cell, one window.open) passed on base: the host's row routing is unchanged; only the name cell's own anchor swallowed the second click. The CONTROL cases passed on both trees.

Ablations (scripts/ablation-replace.mjs, each anchor 1 → 0, restored to the HEAD blob with git diff HEAD empty; ui vitest aliases @object-ui/* to src/, so no dist is in the path):

six legs, each red, each restored
A  objectGridLinkCell.tsx  `const own = opens === false` → `=== "ABL-A"`        4 failed | 3 passed (7)  name cell wears the link face
B1 ObjectGrid.tsx  `const opens = rowOpensRecord !== false` → `!== ("ABL-B1" as unknown)`   2 failed | 5 passed (7)  expected [ 'row-context-open' ] to deeply equal []
B2 ObjectGrid.tsx  `const openHref = rowOpensRecord === false ? ...` → `=== ("ABL-B2" as unknown)`   1 failed | 6 passed (7)  expected [ 'row-context-open-new-tab' ] to deeply equal []  (authored none only: a list-only row has no URL)
C1 ObjectView.tsx  `rowOpensRecord: navOverlay.mode !== 'none'` → `!== ('ABL-C1' as string)`   3 failed | 4 passed (7)
C2 ObjectDataPage.tsx  `rowOpensRecord={recordServed}` → `{recordServed || "ABL-C2".length > 0}`   1 failed | 6 passed (7)
C3 InterfaceListPage.tsx  `rowOpensRecord={recordAction !== 'none'}` → `!== ('ABL-C3' as string)`   1 failed | 1 passed (2)

Acceptance notes

  • Clause-② measured yes (widening). The order allowed a new optional prop when the grid cannot derive the signal; it needed one on ObjectGrid and one on ListView (the hop between the console and the grid), both published, both optional, absent = unchanged. The claim started at no; the seat amends it.
  • File surface. ui/packages/plugin-list/src/ListView.tsx (forwarding only) and three docs files are outside the claim's named files; they are the props that carry the signal to the grid, and the docs-driven rule in ui/AGENTS.md (Coding Standards, rule 2) asks for the README and docs page.
  • ⌘-click on an authored none row still opens a new tab. That is useNavigationOverlay's modifier-click rule ("regardless of the configured navigation mode"), reached through the host's onRowClick; the order says not to break what the host routes through it, so it stays, and the pin holds it. The menu's "Open in new tab" is gone for such a row, so the two now differ; a list-only row opens no tab either way.
  • Other faces of the same rows, not changed here: the phone card face (below 768 px) still draws the card as a tap target, and the gallery and board cards of a none list keep their pointer; only the grid is handed rowOpensRecord. Observed by reading, not measured; noted, not filed.
  • preventNavigation on the console object list: the console's handleRowClick writes ?recordId= for a drawer view without reading preventNavigation, so rowOpensRecord is keyed to the resolved mode only. Observed by reading, not measured; noted, not filed.
  • Lint: eslint on the touched files: 0 errors; the ListView forwarding line adds one react-hooks/refs warning of the same shape its three sibling viewComponentSchema.type === 'object-grid' lines already carry.

Verification

Head a02d289c (origin/main 811b976a merged in; no overlap with this diff). Local subset from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, reconciled with --ran:

69 derived, 69 run, all exit 0; --ran: 0 NOT-MEASURED, 0 UNRUN
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/snapshot-next.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Also run locally, all exit 0 under the verify lock: pnpm --filter @object-ui/plugin-grid run type-check, @object-ui/plugin-list and @object-ui/app-shell (each tsc --noEmit && tsc -p tsconfig.test.json, so the new tests are type-checked); pnpm --dir ui exec vitest run packages/plugin-grid/ → 281 files, 2436 passed; packages/plugin-list/ → 173 files, 1856 passed; the app-shell ObjectView*, objectView*, InterfaceListPage* and ObjectDataPage* files → 111 files, 986 passed; the two new pin files plus ObjectView.listOnlyRecordPage-33219 and rowContextMenu-28434 on the final head → 4 files, 36 passed. pnpm --dir ui check:new-line-citations → 0 new. The full app-shell suite and every other family are CI's.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (touched files linted, 0 errors; the repo-wide run is CI's)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (the 维护者速读 section is the seat format)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…k face or Open item

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pN9UhYM1KGHApVGCH
…k and no Open item

The console hosts (object list, data page, interface list) pass the grid
rowOpensRecord: false when the row navigation resolves to none (an authored
navigation.mode none, or a list-only object), through ListView.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pN9UhYM1KGHApVGCH
…in the row click a plain name cell hands to the row

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pN9UhYM1KGHApVGCH
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pN9UhYM1KGHApVGCH
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 15, 2026
@objectstack-fleet objectstack-fleet Bot added the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Nov 15, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nWoa1Ze1QzjRTSYXD

Contract review

Served-tier: TIER_DEFAULT
Head-sha: a02d289c2c7e2afd7669e656a824899311981666
Local-runs: none

Read: card #33317 (body; comments 6880998774 triage, 6885385302 claim, 6886357996 os-dev-report, 6886368569 claim amendment), PR #33544 (body, 14-file list), the net diff origin/main...a02d289c (merge-base 811b976a; 14 files, +585/-9, identical to the PR file list), and the head's check-runs. Code outside the diff (the hosts' mode resolution, useNavigationOverlay, ListView's navigation, ObjectGridRenderer) was read to judge the hosts, not reviewed.

① Derived judgments

Public surface (accept-set widening, two published types, nothing removed or narrowed):

  • ObjectGridComponentProps.rowOpensRecord?: boolean (@object-ui/plugin-grid, exported from src/index.tsx as ObjectGridComponentProps and its alias ObjectGridProps): RIGHT. Every read is strict on false: LinkCell branches on opens === false; renderRowContextMenu computes opens = rowOpensRecord !== false && (old expression) and openHref = rowOpensRecord === false ? null : newTabHref. With the prop absent (or true), both reduce to the expressions on main, so every other host (Studio designer, embedded renderers, standalone grids, ObjectGridRenderer spreads) draws exactly today's link cell and menu. useObjectGridGenerateColumns gains it in its dep array; while it is absent it stays undefined, so no extra column regeneration. The prop reaches the grid only as a React prop: ObjectGridRenderer passes rest (props, not schema keys), so no authored node key can set it. That matches Metadata-protocol: none.
  • ListViewProps.rowOpensRecord?: boolean (@object-ui/plugin-list, exported type): RIGHT. It is forwarded only when viewComponentSchema.type === 'object-grid' && rowOpensRecord !== undefined, on the same host channel as recordLinkAdornment. Absent means the grid never receives the key and behaves as it does today. Kanban, gallery and calendar never receive it, which the docs state ("Only the grid is handed it").
  • LinkCell opens?: internal. LinkCell is not exported from the plugin-grid index, so this is not public surface. The opens === false branch draws a bare span with the clamp and width classes only: no role, no tabIndex, no handler, no data-testid. The click falls through to the data-table row, so onRowClick routing is unchanged. This is RIGHT for the card's acceptance 1.
  • useObjectViewListRenderer input rowOpensRecord: boolean (required): RIGHT. It is internal to the private @object-ui/app-shell, it is not in its index, and its only caller is ObjectView.tsx:1064.

Host computations, checked against what each host's row click actually does:

  • ObjectView: rowOpensRecord: navOverlay.mode !== 'none': RIGHT. useNavigationOverlay returns mode = navigation?.mode ?? 'drawer', and detailNavigation in objectViewRecordNavigation.ts resolves as follows:
    • A list-only object (!servesRecordRead) resolves to { mode: 'none' } before any authored config, so the result is false. This matches handleRowClick's if (!served) return, which opens nothing, not even on a modifier click.
    • An authored navigation.mode: 'none' on a served object resolves to the authored config spread with a width, and mode stays 'none', so the result is false.
    • On a phone, authored none resolves to page, so the result is true. That is honest, because the phone row does open a page.
    • drawer, page and new_window resolve to true, which matches main.
  • ObjectDataPage: rowOpensRecord={recordServed}: RIGHT. The data page hard-codes { mode: 'drawer' } and ignores authored view navigation, and handleRecordClick returns early only on !recordServed. So recordServed is exactly "the row opens something". An authored none does not reach this page, and the data page correctly keeps its link for it.
  • InterfaceListPage: rowOpensRecord={recordAction !== 'none'}: RIGHT. recordAction is 'none' both for an authored recordAction: 'none' and for a list-only source (!recordServed). In that case onRowClick is already undefined. ListView's own useNavigationOverlay then gets navigation: undefined, because the page's list schema carries no navigation key. With no onNavigate, its handleClick takes the !navigation branch and does nothing. So the row really opens nothing, and false is honest.

Card acceptance:

  1. Plain first cell, plus any link: true column, with no Open or Open in new tab, and row actions kept: met.
  2. Pins for both faces plus CONTROLs: met. There are 9 cases across two files. The dev's red-first run on base f068cde8 gave 5 failed and 4 passed, and six ablation legs were each red.
  3. No change for rows that open: met, by the strict-false reads and by the drawer and page CONTROLs.

Nits (no effect on the contract):

  • (nit) In the opens === false branch, children render outside RecordLink's unlinked RelatedRecordActionsProvider. A lookup-typed or referenceVia value in a record-link column of a none row therefore now draws its own ReferencedRecordLink anchor to the referenced record. That is a real destination, so it is not a dead affordance, but the docs' "drawn as plain text" does not hold for such a cell, and no pin covers it.
  • (nit) The new ⌘-click pin cements, with the words "as before", the useNavigationOverlay modifier rule for an authored none row. See ③.

② Semver level

  • Changeset .changeset/33317-row-opens-nothing-plain.md gives @object-ui/plugin-grid, @object-ui/plugin-list, @object-ui/app-shell and @object-ui/console a patch each. Source or types moved in plugin-grid (public type plus implementation), plugin-list (public type plus forwarding) and app-shell (private, privatePackages.version: true, shipped inside @object-ui/console). Every one has an entry, and console carries the app-shell change. content/docs/ publishes no package. MATCHES.
  • Level: Metadata-protocol: none (no spec key, no node key, packages/spec untouched), and the change is a non-protocol widening. Under AGENTS.md (Before Submitting, item 3), that kind takes patch. patch is RIGHT, and no skip-changeset is warranted, since three packages publish.
  • Clause-②:: the PR body and the changeset body both say yes (widening). Two optional members are added to two exported types, and nothing is removed, renamed, narrowed or re-defaulted. CORRECT. The claim's original no was amended by the seat in comment 6886368569, which matches the diff.
  • Gates on the head (read at review time; 58 check-runs reported; none failed):
    • Success: Check Changeset (both runs), Check PR Size, PR-body guards, Check Documentation Links, Spec property liveness, Governed Surface Queue Guard, filter, Type Check · workspace, Type Check · debt ledger, Type Check · source gates, Lint · ESLint, Dogfood Regression Gate, Live E2E UI, Build UI Deps, Plan Test UI shards, Plan Test Core shards, Test Core (3/8), Auto Label.
    • Skipped: the opt-in harnesses, Build Core, UI Published Artifacts, Dogfood Verify CLI, Temporal Conformance, the queue and schedule verdicts.
    • PENDING (in progress): Type Check UI, Lint UI, Build & E2E UI, Test UI (1–11/16 running; 12–16 not yet listed), Test Core (1/8, 2/8), Build Docs, Type Check · consumer gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, Lint · scoped · slot lookup & corpus.
    • The UI families (Type Check UI, Lint UI, Test UI, Build & E2E UI) are the ones that exercise this diff. This verdict does not stand in for them: landing waits on them.

③ Boundary flags

  • open_questions: []. Nothing to answer.
  • Deviation: Clause-② measured yes (widening): answered. The seat amended the claim (6886368569), and ② above confirms it.
  • Deviation: file surface (ListView.tsx forwarding, plugin-grid README, plugin-list README, plugin-grid.mdx): answered. The amendment adds all four. ListView is the only hop from the console to the grid, and the docs follow ui/AGENTS.md coding standard 2. No packages/spec or server file is touched, so the claim's exclusion holds.
  • Deviation: no assignees on pr_create: answered. The order and AGENTS.md say none, and it does not affect the contract.
  • Out of scope: phone card face and gallery/board cards of a none list keep their tap target or pointer: ESCALATE to the seat to file, after a dedupe on the dev's words. It is the same dead affordance on sibling faces. The card's acceptance names the grid only, so it is correctly outside this PR.
  • Out of scope: console handleRowClick writes ?recordId= for a drawer view without reading navigation.preventNavigation: ESCALATE to the seat to file. It is pre-existing. Keying rowOpensRecord to the resolved mode is honest today, because such a row does open the drawer. If preventNavigation is later honoured, the ObjectView host must fold it into rowOpensRecord too.
  • Out of scope: ⌘-click on an authored-none row still opens a new tab, while the menu no longer offers Open in new tab: ESCALATE as a product question for the maintainer: does authored navigation.mode: 'none' mean no record opens at all, modifier clicks included? Not blocking: the order said not to break host routing through onRowClick, and the behaviour is pre-existing. Whoever answers should know that ObjectView.rowOpensNothing-33317.test.tsx now pins the new-tab open, so a "no open at all" ruling must change that pin. A list-only row opens no tab either way.
  • Lint note: one added react-hooks/refs warning in ListView, the same shape as its three sibling forwarding lines: accepted. Lint · ESLint is green on the head.

Implemented-by: claude/project-thread-9khlxt
Reviewed-by: session_01FEkQ5nWoa1Ze1QzjRTSYXD

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nWoa1Ze1QzjRTSYXD

席位核对

Verdict: ACCEPT · head a02d289c · 2026-11-15T00:29Z

  1. Shape. Draft → main, Fixes #33317. 14 files, +585/−9:

    • ui/packages/plugin-grid/src/ (ObjectGrid.tsx, objectGridLinkCell.tsx, objectGridGenerateColumns.tsx)
    • ui/packages/plugin-list/src/ListView.tsx (forwarding only)
    • the three console hosts in ui/packages/app-shell/src/views/ (ObjectView via objectViewListRenderer, ObjectDataPage, InterfaceListPage) and two new rendered tests
    • docs: the plugin-grid and plugin-list READMEs and content/docs/frontend/plugins/plugin-grid.mdx
    • one root changeset (plugin-grid, plugin-list, app-shell, console patch)

    All of it is on the claimed surface as amended (6886368569).

  2. Premise. Measured red by the dev on f068cde8. An authored none row drew an <a href> first cell plus Open and Open in new tab. A list-only row drew span role="link" plus Open.

    • The fix: an optional rowOpensRecord on ObjectGridComponentProps and ListViewProps. It acts only on an explicit false, and ListView forwards it only when it is set, so every other host is unchanged.
    • With false, the first cell is plain text, and the row menu drops Open and Open in new tab but keeps its row actions. The click still falls through to the host's onRowClick.
    • Each console host computes the prop from what its row click really does:
      • ObjectView: the resolved navigation mode is 'none' for authored none and for list-only objects. On a phone, authored none resolves to page, so the row stays openable.
      • ObjectDataPage: recordServed. The page ignores authored navigation and opens its drawer.
      • InterfaceListPage: its own record action.
    • Pins: both faces for authored none and list-only, plus CONTROLs for navigable rows. Six ablation legs went red and were restored.
  3. Contract record. PASS 6886428303 on this head: two additive optional props, Clause-②: yes (widening), Metadata-protocol: none, patch right for every moved package.

  4. Gates on head a02d289c.

    • All 65 check runs are success or skipped, including Test UI shards + aggregate, Test Core, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Build Docs, Lint & Repo Gates, TypeScript Type Check, Check Changeset and PR-body guards.
    • check-governed-merges.mjs --pr 33544: NOT governed, 594 lines ≤ 3000.
    • The dev's local --ran is 69/69, 0 NOT-MEASURED.
  5. Nit (no change asked). On a row that opens nothing, a lookup or referenceVia value in the link column still draws its own anchor to the referenced record. That target is real, so the link is not dead, but the docs' "plain text" wording does not cover those cells.

  6. K. 8 first-parent commits since base 811b976a < 15, so no base sync.

  7. Out of scope (carded, p3, serial after this PR).

  8. Authority. Landing under the maintainer's standing authorisation 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-project-manager · session_01FEkQ5nWoa1Ze1QzjRTSYXD

Landing · head a02d289c (the ACCEPTed head; K = 8, no sync) · 2026-11-15T00:30Z

reading value
check runs on a02d289c 65, all success or skipped
contract record PASS 6886428303

State: ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 15, 2026 00:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 15, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant